package cz.possoft.explorer;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.catalina.Container;
import org.apache.catalina.ContainerServlet;
import org.apache.catalina.Engine;
import org.apache.catalina.Wrapper;

import cz.possoft.explorer.context.ObjectContext;
import cz.possoft.explorer.context.ObjectContextBuilder;
import cz.possoft.explorer.converter.ConverterException;
import cz.possoft.explorer.meta.ClassMetadata;
import cz.possoft.explorer.resolver.Resolver;
import cz.possoft.explorer.resolver.method.MethodKey;
import cz.possoft.explorer.resolver.method.MethodResolver;
import cz.possoft.explorer.utils.Utils;

/**
 * <p>Servlet can be used to read instance of Tomcat engine object (thanks to ContainerServlet interface and usage of same classloader which is used for Tomcat classes)
 * After it we are able to read state of other objects which are accessible from Engine instance. 
 * <p>Servlet practically uses framework for building object informations at runtime.
 * 
 * @author <a href="mailto:mposolda@gmail.com">Marek Posolda</a>
 * @version $Revision$
 */
public class ExplorerServlet extends HttpServlet implements ContainerServlet
{
   public static final String HISTORY_INDEX_ATTRIBUTE_NAME = "historyIndex";

   public static final String CURRENT_CONTEXT_REQUEST_ATTRIBUTE_NAME = "currentContext";

   public static final String CURRENT_METADATA_REQUEST_ATTRIBUTE_NAME = "currentMetadata";

   private static final String OBJECT_CONTEXT_JSP_PATH = "/WEB-INF/jsp/objectContext.jsp";

   private static final String METHODS_JSP_PATH = "/WEB-INF/jsp/methods.jsp";

   private static final int MAX_LIST_CAPACITY = 15;

   private Wrapper wrapper;

   private Engine engine;

   // shared map where key is sessionId and value is list with current object for current client on index 0 and history for this client (indexes 1..N)
   private ConcurrentMap<String, List<Object>> clientToObjectContext = new ConcurrentHashMap<String, List<Object>>();

   // length of history for list in clientToObjectContext. This means length of history for each client.
   private static final int MAX_HISTORY_LENGTH = 10;

   public Wrapper getWrapper()
   {
      return wrapper;
   }

   public void setWrapper(Wrapper wrapper)
   {
      this.wrapper = wrapper;

      // Find engine from wrapper
      Container engine = wrapper;
      while (engine instanceof Engine != true)
      {
         engine = engine.getParent();
      }
      this.engine = (Engine) engine;
   }

   protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
   {
      try
      {
         response.setContentType("text/html");
         
         // obtain ObjectContextBuilder instance
         ObjectContextBuilder ocBuilder = ServiceFactory.getInstance().getObjectContextBuilder();

         // obtain sessionId
         String sessionId = request.getSession().getId();

         // obtain current history index
         Integer historyIndex = (Integer) request.getSession().getAttribute(HISTORY_INDEX_ATTRIBUTE_NAME);
         if (historyIndex == null)
         {
            historyIndex = -1;
         }
         int oldHistoryIndex = historyIndex;

         // Try to read new active object from request attribute and add object to map if not null. 
         String newFieldName = request.getParameter("fieldName");
         if (newFieldName != null)
         {
            System.out.println("Creating new ObjectContext for value of field " + newFieldName);
            Object current = getFromMap(sessionId, historyIndex);
            ClassMetadata<?, ?> metadata = ServiceFactory.getInstance().getDomainMetadata().getClassMetadata(
                  current.getClass());

            // TODO: type-safety here
            Resolver resolver = metadata.getResolver();

            // now we need to decode newFieldName from String to correct type required by Resolver
            Class<?> requestedClass = resolver.getKeyClass();
            Object o = ServiceFactory.getInstance().getConverter().convert(
                  newFieldName, requestedClass);

            // now get converter registry and encode it to correct type.
            Object newObject = resolver.getFieldValue(current, o);
            if (newObject == null)
            {
               response.getWriter().println(
                     "Field " + newFieldName + " is null for object " + current + ". Go back please.");
               return;
            }
            else
            {
               putToMap(sessionId, newObject, historyIndex);
               historyIndex++;
               request.getSession().setAttribute(HISTORY_INDEX_ATTRIBUTE_NAME, historyIndex);
            }
         }

         // Try to read methodName attribute from request and check if request is processed or not.
         String methodNameParam = request.getParameter("methodName");
         if (methodNameParam != null)
         {
            System.out.println("Method name: " + methodNameParam + ", parameters: " + request.getParameterMap());
            Object operationOutput = invokeMethod(getFromMap(sessionId, historyIndex), methodNameParam, request);

            // 1) Method output is null 
            if (operationOutput == null)
            {
               response.getWriter().println(
                     "Operation " + methodNameParam
                           + " invoked. Output is <B>null</B><BR />Go back please.");
               return;
            }
            // 2) Method output is not null but output class is not registered in our framework
            else if (ServiceFactory.getInstance().getDomainMetadata().getClassMetadata(operationOutput.getClass()) == null)
            {
               // TODO: use converter instead of toString()
               response.getWriter().println(
                     "Operation " + methodNameParam + " invoked. Output is <B>"
                           + operationOutput + "</B><BR />Go back please.");
               return;
            }
            // 3) operationOutput class is registered in framework and so we can process it.
            else
            {
               putToMap(sessionId, operationOutput, historyIndex);
               historyIndex++;
               request.getSession().setAttribute(HISTORY_INDEX_ATTRIBUTE_NAME, historyIndex);
            }
         }

         // Save engine for case that no object has been assigned previously (this is usually the case for first HTTP request)
         if (historyIndex == -1)
         {
            putToMap(sessionId, engine, historyIndex);
            historyIndex++;
            request.getSession().setAttribute(HISTORY_INDEX_ATTRIBUTE_NAME, historyIndex);
         }

         // Try to read "back" button
         String back = request.getParameter("back");
         if ((back != null) && (historyIndex > 0))
         {
            historyIndex--;
            request.getSession().setAttribute(HISTORY_INDEX_ATTRIBUTE_NAME, historyIndex);
         }

         // log history index
         System.out.println("History index at startup of request: " + oldHistoryIndex
               + ", history index at end of request: " + historyIndex);

         // Obtain current object from map and create list of ObjectContexts
         Object object = getFromMap(sessionId, historyIndex);
         List<ObjectContext<? super Object, Object>> objectContextList = ocBuilder.build(object);

         // Now render current ObjectContext list
         Object mainObject = object;
         response.getWriter().println(
               "<h2>" + mainObject + "</h2><br>Object: <b>" + mainObject + "</b><br>Class: <b>"
                     + mainObject.getClass().getName() + "</b><hr />");
         for (ObjectContext<?, ?> currentObjectContext : objectContextList)
         {
            ClassMetadata<?, ?> metadataOfCurrentObject = ServiceFactory.getInstance().getDomainMetadata()
                  .getClassMetadata(currentObjectContext.getClazz());

            request.setAttribute(CURRENT_CONTEXT_REQUEST_ATTRIBUTE_NAME, currentObjectContext);
            request.getRequestDispatcher(OBJECT_CONTEXT_JSP_PATH).include(request, response);

            request.setAttribute(CURRENT_METADATA_REQUEST_ATTRIBUTE_NAME, metadataOfCurrentObject);
            request.getRequestDispatcher(METHODS_JSP_PATH).include(request, response);
         }

         // TODO: encode properly
         response.getWriter().println("<br><a href=\"explorer?back=true\">Back to previous object</a>");

      }
      catch (ConverterException ce)
      {
         throw new ServletException(ce);
      }
   }

   /**
    * get object from clientToObjectContext map from given index (index 0 means current object in actual session)
    * Indexes 1..N contains history for current client, so that we can obtain older objects from previous HTTP requests for this client.
    * 
    * @param sessionId
    * @param index
    * @return
    */
   private Object getFromMap(String sessionId, int index)
   {
      List<Object> list = clientToObjectContext.get(sessionId);
      if (list == null)
      {
         list = new ArrayList<Object>();
         clientToObjectContext.put(sessionId, list);
         return null;
      }
      return list.get(index);
   }

   /**
    * Add object into map and move other objects to history (shift all objects by 1)
    * 
    * @param sessionId
    * @param object
    */
   private void putToMap(String sessionId, Object object, int historyIndex)
   {
      // obtain list or create it if necessary
      List<Object> list = clientToObjectContext.get(sessionId);
      if (list == null)
      {
         list = new ArrayList<Object>();
         clientToObjectContext.put(sessionId, list);
      }

      historyIndex++;

      // remove previous (dead) objects from history
      while (list.size() > historyIndex)
      {
         list.remove(list.size() - 1);
      }

      // add new object to the end of the list
      list.add(object);
   }

   private Object invokeMethod(Object currentObject, String methodName, HttpServletRequest request)
   {
      try
      {
         List<Object> parameters = new ArrayList<Object>(MAX_LIST_CAPACITY);
         List<Class<?>> paramClasses = new ArrayList<Class<?>>(MAX_LIST_CAPACITY);
         for (int i = 0; i < MAX_LIST_CAPACITY; i++)
         {
            parameters.add(null);
            paramClasses.add(null);
         }

         int maxParamIndex = 0;
         for (Object param : request.getParameterMap().keySet())
         {
            String paramName = (String) param;

            // we need only params starting with "par"
            if (!paramName.startsWith("par"))
            {
               continue;
            }
            paramName = paramName.substring(3);

            Integer paramNumber = Integer.parseInt(paramName.substring(0, paramName.indexOf('_')));
            String className = paramName.substring(paramName.indexOf('_') + 1);
            String paramValue = ((String[]) request.getParameterMap().get(param))[0];
            System.out.println("Parameter:" + paramNumber + "," + className + "," + paramValue);

            if (paramNumber > maxParamIndex)
            {
               maxParamIndex = paramNumber;
            }

            Class<?> clazz = Utils.findClassFromName(className);

            paramClasses.set(paramNumber, clazz);            
            parameters.set(paramNumber, ServiceFactory.getInstance().getConverter().convert(paramValue, clazz));
         }

         // Now convert lists back to arrays
         Object[] params = new Object[maxParamIndex];
         Class<?>[] paramClassess = new Class<?>[maxParamIndex];
         for (int i = 0; i < maxParamIndex; i++)
         {
            params[i] = parameters.get(i + 1);
            paramClassess[i] = paramClasses.get(i + 1);
         }

         // Now invoke method with usage of methodResolver
         ClassMetadata<?, ?> currentClassMetadata = ServiceFactory.getInstance().getDomainMetadata().getClassMetadata(
               currentObject.getClass());
         MethodResolver methodResolver = currentClassMetadata.getMethodResolver();
         return methodResolver.invokeMethod(currentObject, new MethodKey(methodName, paramClassess), params);
      }
      catch (RuntimeException e)
      {
         throw e;
      }
      catch (Exception e)
      {
         throw new RuntimeException(e);
      }
   }

}
